<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace Think\Model;

use Think\Model;

/**
 * ThinkPHP关联模型扩展
 */
class RelationModel extends Model
{

    const HAS_ONE = 1;

    const BELONGS_TO = 2;

    const HAS_MANY = 3;

    const MANY_TO_MANY = 4;
    
    // 关联定义
    protected $_link = array();

    /**
     * 动态方法实现
     * 
     * @access public
     * @param string $method
     *            方法名称
     * @param array $args
     *            调用参数
     * @return mixed
     */
    public function __call($method, $args)
    {
        if (strtolower(substr($method, 0, 8)) == 'relation') {
            $type = strtoupper(substr($method, 8));
            if (in_array($type, array(
                'ADD',
                'SAVE',
                'DEL'
            ), true)) {
                array_unshift($args, $type);
                return call_user_func_array(array(
                    &$this,
                    'opRelation'
                ), $args);
            }
        } else {
            return parent::__call($method, $args);
        }
    }

    /**
     * 得到关联的数据表名
     * 
     * @access public
     * @return string
     */
    public function getRelationTableName($relation)
    {
        $relationTable = ! empty($this->tablePrefix) ? $this->tablePrefix : '';
        $relationTable .= $this->tableName ? $this->tableName : $this->name;
        $relationTable .= '_' . $relation->getModelName();
        return strtolower($relationTable);
    }
    
    // 查询成功后的回调方法
    protected function _after_find(&$result, $options)
    {
        // 获取关联数据 并附加到结果中
        if (! empty($options['link']))
            $this->getRelation($result, $options['link']);
    }
    
    // 查询数据集成功后的回调方法
    protected function _after_select(&$result, $options)
    {
        // 获取关联数据 并附加到结果中
        if (! empty($options['link']))
            $this->getRelations($result, $options['link']);
    }
    
    // 写入成功后的回调方法
    protected function _after_insert($data, $options)
    {
        // 关联写入
        if (! empty($options['link']))
            $this->opRelation('ADD', $data, $options['link']);
    }
    
    // 更新成功后的回调方法
    protected function _after_update($data, $options)
    {
        // 关联更新
        if (! empty($options['link']))
            $this->opRelation('SAVE', $data, $options['link']);
    }
    
    // 删除成功后的回调方法
    protected function _after_delete($data, $options)
    {
        // 关联删除
        if (! empty($options['link']))
            $this->opRelation('DEL', $data, $options['link']);
    }

    /**
     * 对保存到数据库的数据进行处理
     * 
     * @access protected
     * @param mixed $data
     *            要操作的数据
     * @return boolean
     */
    protected function _facade($data)
    {
        $this->_before_write($data);
        return $data;
    }

    /**
     * 获取返回数据集的关联记录
     * 
     * @access protected
     * @param array $resultSet
     *            返回数据
     * @param string|array $name
     *            关联名称
     * @return array
     */
    protected function getRelations(&$resultSet, $name = '')
    {
        // 获取记录集的主键列表
        foreach ($resultSet as $key => $val) {
            $val = $this->getRelation($val, $name);
            $resultSet[$key] = $val;
        }
        return $resultSet;
    }

    /**
     * 获取返回数据的关联记录
     * 
     * @access protected
     * @param mixed $result
     *            返回数据
     * @param string|array $name
     *            关联名称
     * @param boolean $return
     *            是否返回关联数据本身
     * @return array
     */
    protected function getRelation(&$result, $name = '', $return = false)
    {
        if (! empty($this->_link)) {
            foreach ($this->_link as $key => $val) {
                $mappingName = ! empty($val['mapping_name']) ? $val['mapping_name'] : $key; // 映射名称
                if (empty($name) || true === $name || $mappingName == $name || (is_array($name) && in_array($mappingName, $name))) {
                    $mappingType = ! empty($val['mapping_type']) ? $val['mapping_type'] : $val; // 关联类型
                    $mappingClass = ! empty($val['class_name']) ? $val['class_name'] : $key; // 关联类名
                    $mappingFields = ! empty($val['mapping_fields']) ? $val['mapping_fields'] : '*'; // 映射字段
                    $mappingCondition = ! empty($val['condition']) ? $val['condition'] : '1=1'; // 关联条件
                    $mappingKey = ! empty($val['mapping_key']) ? $val['mapping_key'] : $this->getPk(); // 关联键名
                    if (strtoupper($mappingClass) == strtoupper($this->name)) {
                        // 自引用关联 获取父键名
                        $mappingFk = ! empty($val['parent_key']) ? $val['parent_key'] : 'parent_id';
                    } else {
                        $mappingFk = ! empty($val['foreign_key']) ? $val['foreign_key'] : strtolower($this->name) . '_id'; // 关联外键
                    }
                    // 获取关联模型对象
                    $model = D($mappingClass);
                    switch ($mappingType) {
                        case self::HAS_ONE:
                            $pk = $result[$mappingKey];
                            $mappingCondition .= " AND {$mappingFk}='{$pk}'";
                            $relationData = $model->where($mappingCondition)
                                ->field($mappingFields)
                                ->find();
                            if (! empty($val['relation_deep'])) {
                                $model->getRelation($relationData, $val['relation_deep']);
                            }
                            break;
                        case self::BELONGS_TO:
                            if (strtoupper($mappingClass) == strtoupper($this->name)) {
                                // 自引用关联 获取父键名
                                $mappingFk = ! empty($val['parent_key']) ? $val['parent_key'] : 'parent_id';
                            } else {
                                $mappingFk = ! empty($val['foreign_key']) ? $val['foreign_key'] : strtolower($model->getModelName()) . '_id'; // 关联外键
                            }
                            $fk = $result[$mappingFk];
                            $mappingCondition .= " AND {$model->getPk()}='{$fk}'";
                            $relationData = $model->where($mappingCondition)
                                ->field($mappingFields)
                                ->find();
                            if (! empty($val['relation_deep'])) {
                                $model->getRelation($relationData, $val['relation_deep']);
                            }
                            break;
                        case self::HAS_MANY:
                            $pk = $result[$mappingKey];
                            $mappingCondition .= " AND {$mappingFk}='{$pk}'";
                            $mappingOrder = ! empty($val['mapping_order']) ? $val['mapping_order'] : '';
                            $mappingLimit = ! empty($val['mapping_limit']) ? $val['mapping_limit'] : '';
                            // 延时获取关联记录
                            $relationData = $model->where($mappingCondition)
                                ->field($mappingFields)
                                ->order($mappingOrder)
                                ->limit($mappingLimit)
                                ->select();
                            if (! empty($val['relation_deep'])) {
                                foreach ($relationData as $key => $data) {
                                    $model->getRelation($data, $val['relation_deep']);
                                    $relationData[$key] = $data;
                                }
                            }
                            break;
                        case self::MANY_TO_MANY:
                            $pk = $result[$mappingKey];
                            $prefix = $this->tablePrefix;
                            $mappingCondition = " {$mappingFk}='{$pk}'";
                            $mappingOrder = $val['mapping_order'];
                            $mappingLimit = $val['mapping_limit'];
                            $mappingRelationFk = $val['relation_foreign_key'] ? $val['relation_foreign_key'] : $model->getModelName() . '_id';
                            if (isset($val['relation_table'])) {
                                $mappingRelationTable = preg_replace_callback("/__([A-Z_-]+)__/sU", function ($match) use($prefix) {
                                    return $prefix . strtolower($match[1]);
                                }, $val['relation_table']);
                            } else {
                                $mappingRelationTable = $this->getRelationTableName($model);
                            }
                            $sql = "SELECT b.{$mappingFields} FROM {$mappingRelationTable} AS a, " . $model->getTableName() . " AS b WHERE a.{$mappingRelationFk} = b.{$model->getPk()} AND a.{$mappingCondition}";
                            if (! empty($val['condition'])) {
                                $sql .= ' AND ' . $val['condition'];
                            }
                            if (! empty($mappingOrder)) {
                                $sql .= ' ORDER BY ' . $mappingOrder;
                            }
                            if (! empty($mappingLimit)) {
                                $sql .= ' LIMIT ' . $mappingLimit;
                            }
                            $relationData = $this->query($sql);
                            if (! empty($val['relation_deep'])) {
                                foreach ($relationData as $key => $data) {
                                    $model->getRelation($data, $val['relation_deep']);
                                    $relationData[$key] = $data;
                                }
                            }
                            break;
                    }
                    if (! $return) {
                        if (isset($val['as_fields']) && in_array($mappingType, array(
                            self::HAS_ONE,
                            self::BELONGS_TO
                        ))) {
                            // 支持直接把关联的字段值映射成数据对象中的某个字段
                            // 仅仅支持HAS_ONE BELONGS_TO
                            $fields = explode(',', $val['as_fields']);
                            foreach ($fields as $field) {
                                if (strpos($field, ':')) {
                                    list ($relationName, $nick) = explode(':', $field);
                                    $result[$nick] = $relationData[$relationName];
                                } else {
                                    $result[$field] = $relationData[$field];
                                }
                            }
                        } else {
                            $result[$mappingName] = $relationData;
                        }
                        unset($relationData);
                    } else {
                        return $relationData;
                    }
                }
            }
        }
        return $result;
    }

    /**
     * 操作关联数据
     * 
     * @access protected
     * @param string $opType
     *            操作方式 ADD SAVE DEL
     * @param mixed $data
     *            数据对象
     * @param string $name
     *            关联名称
     * @return mixed
     */
    protected function opRelation($opType, $data = '', $name = '')
    {
        $result = false;
        if (empty($data) && ! empty($this->data)) {
            $data = $this->data;
        } elseif (! is_array($data)) {
            // 数据无效返回
            return false;
        }
        if (! empty($this->_link)) {
            // 遍历关联定义
            foreach ($this->_link as $key => $val) {
                // 操作制定关联类型
                $mappingName = $val['mapping_name'] ? $val['mapping_name'] : $key; // 映射名称
                if (empty($name) || true === $name || $mappingName == $name || (is_array($name) && in_array($mappingName, $name))) {
                    // 操作制定的关联
                    $mappingType = ! empty($val['mapping_type']) ? $val['mapping_type'] : $val; // 关联类型
                    $mappingClass = ! empty($val['class_name']) ? $val['class_name'] : $key; // 关联类名
                    $mappingKey = ! empty($val['mapping_key']) ? $val['mapping_key'] : $this->getPk(); // 关联键名
                                                                                                    // 当前数据对象主键值
                    $pk = $data[$mappingKey];
                    if (strtoupper($mappingClass) == strtoupper($this->name)) {
                        // 自引用关联 获取父键名
                        $mappingFk = ! empty($val['parent_key']) ? $val['parent_key'] : 'parent_id';
                    } else {
                        $mappingFk = ! empty($val['foreign_key']) ? $val['foreign_key'] : strtolower($this->name) . '_id'; // 关联外键
                    }
                    if (! empty($val['condition'])) {
                        $mappingCondition = $val['condition'];
                    } else {
                        $mappingCondition = array();
                        $mappingCondition[$mappingFk] = $pk;
                    }
                    // 获取关联model对象
                    $model = D($mappingClass);
                    $mappingData = isset($data[$mappingName]) ? $data[$mappingName] : false;
                    if (! empty($mappingData) || $opType == 'DEL') {
                        switch ($mappingType) {
                            case self::HAS_ONE:
                                switch (strtoupper($opType)) {
                                    case 'ADD': // 增加关联数据
                                        $mappingData[$mappingFk] = $pk;
                                        $result = $model->add($mappingData);
                                        break;
                                    case 'SAVE': // 更新关联数据
                                        $result = $model->where($mappingCondition)->save($mappingData);
                                        break;
                                    case 'DEL': // 根据外键删除关联数据
                                        $result = $model->where($mappingCondition)->delete();
                                        break;
                                }
                                break;
                            case self::BELONGS_TO:
                                break;
                            case self::HAS_MANY:
                                switch (strtoupper($opType)) {
                                    case 'ADD': // 增加关联数据
                                        $model->startTrans();
                                        foreach ($mappingData as $val) {
                                            $val[$mappingFk] = $pk;
                                            $result = $model->add($val);
                                        }
                                        $model->commit();
                                        break;
                                    case 'SAVE': // 更新关联数据
                                        $model->startTrans();
                                        $pk = $model->getPk();
                                        foreach ($mappingData as $vo) {
                                            if (isset($vo[$pk])) { // 更新数据
                                                $mappingCondition = "$pk ={$vo[$pk]}";
                                                $result = $model->where($mappingCondition)->save($vo);
                                            } else { // 新增数据
                                                $vo[$mappingFk] = $data[$mappingKey];
                                                $result = $model->add($vo);
                                            }
                                        }
                                        $model->commit();
                                        break;
                                    case 'DEL': // 删除关联数据
                                        $result = $model->where($mappingCondition)->delete();
                                        break;
                                }
                                break;
                            case self::MANY_TO_MANY:
                                $mappingRelationFk = $val['relation_foreign_key'] ? $val['relation_foreign_key'] : $model->getModelName() . '_id'; // 关联
                                $prefix = $this->tablePrefix;
                                if (isset($val['relation_table'])) {
                                    $mappingRelationTable = preg_replace_callback("/__([A-Z_-]+)__/sU", function ($match) use($prefix) {
                                        return $prefix . strtolower($match[1]);
                                    }, $val['relation_table']);
                                } else {
                                    $mappingRelationTable = $this->getRelationTableName($model);
                                }
                                if (is_array($mappingData)) {
                                    $ids = array();
                                    foreach ($mappingData as $vo)
                                        $ids[] = $vo[$mappingKey];
                                    $relationId = implode(',', $ids);
                                }
                                switch (strtoupper($opType)) {
                                    case 'ADD': // 增加关联数据
                                        if (isset($relationId)) {
                                            $this->startTrans();
                                            // 插入关联表数据
                                            $sql = 'INSERT INTO ' . $mappingRelationTable . ' (' . $mappingFk . ',' . $mappingRelationFk . ') SELECT a.' . $this->getPk() . ',b.' . $model->getPk() . ' FROM ' . $this->getTableName() . ' AS a ,' . $model->getTableName() . " AS b where a." . $this->getPk() . ' =' . $pk . ' AND  b.' . $model->getPk() . ' IN (' . $relationId . ") ";
                                            $result = $model->execute($sql);
                                            if (false !== $result)
                                                // 提交事务
                                                $this->commit();
                                            else
                                                // 事务回滚
                                                $this->rollback();
                                        }
                                        break;
                                    case 'SAVE': // 更新关联数据
                                        if (isset($relationId)) {
                                            $this->startTrans();
                                            // 删除关联表数据
                                            $this->table($mappingRelationTable)
                                                ->where($mappingCondition)
                                                ->delete();
                                            // 插入关联表数据
                                            $sql = 'INSERT INTO ' . $mappingRelationTable . ' (' . $mappingFk . ',' . $mappingRelationFk . ') SELECT a.' . $this->getPk() . ',b.' . $model->getPk() . ' FROM ' . $this->getTableName() . ' AS a ,' . $model->getTableName() . " AS b where a." . $this->getPk() . ' =' . $pk . ' AND  b.' . $model->getPk() . ' IN (' . $relationId . ") ";
                                            $result = $model->execute($sql);
                                            if (false !== $result)
                                                // 提交事务
                                                $this->commit();
                                            else
                                                // 事务回滚
                                                $this->rollback();
                                        }
                                        break;
                                    case 'DEL': // 根据外键删除中间表关联数据
                                        $result = $this->table($mappingRelationTable)
                                            ->where($mappingCondition)
                                            ->delete();
                                        break;
                                }
                                break;
                        }
                        if (! empty($val['relation_deep'])) {
                            $model->opRelation($opType, $mappingData, $val['relation_deep']);
                        }
                    }
                }
            }
        }
        return $result;
    }

    /**
     * 进行关联查询
     * 
     * @access public
     * @param mixed $name
     *            关联名称
     * @return Model
     */
    public function relation($name)
    {
        $this->options['link'] = $name;
        return $this;
    }

    /**
     * 关联数据获取 仅用于查询后
     * 
     * @access public
     * @param string $name
     *            关联名称
     * @return array
     */
    public function relationGet($name)
    {
        if (empty($this->data))
            return false;
        return $this->getRelation($this->data, $name, true);
    }
}